package ru.artyushov.fingers.core.basic;

import org.apache.commons.lang3.tuple.Pair;
import ru.artyushov.fingers.core.FeatureSet;
import ru.artyushov.fingers.core.data.Fingerprint;
import ru.artyushov.fingers.core.data.Point;

import java.util.*;

public class SpecialPointsFeatureSet implements FeatureSet<SpecialPointsFeatureSet> {

    private final Collection<Point> deadEnds;
    private final Collection<Point> forks;
    private final Fingerprint fingerprint;

    private static final Pair<Double, Double> alphaRange = Pair.of(-15 * Math.PI / 180, 15 * Math.PI / 180);
    private static final Double alphaStep = Math.PI / 180;

    private static final Pair<Integer, Integer> dxAndDyRange = Pair.of(-10, 10);
    private static final int dxAndDyStep = 1;

    private static final Pair<Double, Double> scaleRange = Pair.of(0.8, 1.2);
    private static final double scaleStep = 0.1;

    public SpecialPointsFeatureSet(Collection<Point> deadEnds, Collection<Point> forks, Fingerprint fingerprint) {
        this.deadEnds = Collections.unmodifiableCollection(new LinkedHashSet<Point> (deadEnds));
        this.forks = Collections.unmodifiableCollection(new LinkedHashSet<Point> (forks));
        this.fingerprint = fingerprint;
    }

    @Override
    public double getMatchProbability(SpecialPointsFeatureSet other) {

        double minDistance = Double.MAX_VALUE;
        double bestAlpha = 0;
        int bestDx = 0;
        int bestDy = 0;
        double bestScale = 1;
        for (double alpha = alphaRange.getLeft(); alpha <= alphaRange.getRight(); alpha += alphaStep) {
            for (int dx = dxAndDyRange.getLeft(); dx <= dxAndDyRange.getRight(); dx += dxAndDyStep) {
                for (int dy = dxAndDyRange.getLeft(); dy <= dxAndDyRange.getRight(); dy += dxAndDyStep) {
                    for (double scaleCoef = scaleRange.getLeft(); scaleCoef <= scaleRange.getRight(); scaleCoef += scaleStep) {
                        Collection<Point> transformed = translate(other.getForks(), dx, dy);
                        transformed = rotate(transformed, other.getFingerprint().getColNumber() / 2, other.getFingerprint().getRowNumber() / 2, alpha);
                        transformed = scale(transformed, other.getFingerprint().getColNumber() / 2, other.getFingerprint().getRowNumber() / 2, scaleCoef);
                        double distance = distance(forks, transformed);
                        if (distance < minDistance) {
                            minDistance = distance;
                            bestAlpha = alpha;
                            bestDx = dx;
                            bestDy = dy;
                            bestScale = scaleCoef;
                        }
                    }
                }
            }
        }

        System.out.println("Alpha:\t" + bestAlpha);
        System.out.println("Dx:\t" + bestDx);
        System.out.println("Dy:\t" + bestDy);
        System.out.println("Scale:\t" + bestScale);
        return minDistance;
    }

    public Collection<Point> getDeadEnds() {
        return deadEnds;
    }

    public Collection<Point> getForks() {
        return forks;
    }

    public Fingerprint getFingerprint() {
        return fingerprint;
    }

    private Collection<Point> rotate(Collection<Point> points, int xCenter, int yCenter, double alpha) {
        Collection<Point> result = new ArrayList<Point>(points.size());
        for (final Point point : points) {
            int newX = (int) Math.round(xCenter + (point.x - xCenter) * Math.cos(alpha) - (point.y - yCenter) * Math.sin(alpha));
            int newY = (int) Math.round(yCenter + (point.x - xCenter) * Math.sin(alpha) + (point.y - yCenter) * Math.cos(alpha));
            result.add(new Point(newX, newY));
        }
        return result;
    }

    private Collection<Point> translate(Collection<Point> points, int dx, int dy) {
        Collection<Point> result = new ArrayList<Point>(points.size());
        for (final Point point : points) {
            result.add(new Point(point.x + dx, point.y + dy));
        }
        return result;
    }

    private Collection<Point> scale(Collection<Point> points, int xCenter, int yCenter, double coef) {
        Collection<Point> result = new ArrayList<Point>(points.size());
        for (final Point point : points) {
            result.add(new Point((int) Math.round(xCenter + (point.x - xCenter) * coef), (int) Math.round(yCenter + (point.y - yCenter) * coef)));
        }
        return result;
    }

    private double distance(Collection<Point> first, Collection<Point> second) {
        if (first.size() > second.size()) {
            Collection<Point> tmp = second;
            second = first;
            first = tmp;
        }
        List<Double> distances = new ArrayList<Double>(first.size());
        for (final Point point : first) {
            Point closestPoint = getClosestPoint(point, second);
            distances.add(Math.hypot(point.x - closestPoint.x, point.y - closestPoint.y));
        }

        return countMedian(distances);
    }

    private Point getClosestPoint(Point point, Collection<Point> other) {
        Point result = null;
        double minDist = Double.MAX_VALUE;
        for (final Point secondPoint : other) {
            double hypot = Math.hypot(point.x - secondPoint.x, point.y - secondPoint.y);
            if (minDist > hypot) {
                minDist = hypot;
                result = secondPoint;
            }
        }
        return result;
    }

    private double countMedian(List<Double> values) {
        Double[] array = values.toArray(new Double[values.size()]);
        Arrays.sort(array);
        return array[array.length / 2];
    }
}
